#! /usr/bin/env python

import csv
# -*- coding: UTF-8 -*-
# ==============================================================================
# Filename : SerialPage.py
# Purpose  :
# Author   : Yohann Li (lyh001686@sina.com)
# Date     : 2020-07-03 20:23
# ==============================================================================
import os
import sys

# from PyQt5.Qsci import QsciScintilla
from PyQt5.QtCore import Qt, pyqtSignal, QIODevice, QByteArray, QThread, QObject, QTimer, pyqtSlot
from PyQt5.QtGui import QCloseEvent
from PyQt5.QtSerialPort import QSerialPortInfo, QSerialPort
from PyQt5.QtWidgets import QHBoxLayout, QApplication, QVBoxLayout, QMessageBox, QFileDialog

from Widgets.MiniMap import MiniMap
from Widgets.NotificationBox import NoteBox
from Widgets.ReaderPage import Frame
from Widgets.SshEdit import SSHEdit, SSH
from Widgets.TextEdit import TextEdit
from Widgets.treeview import TreeView
# from Widgets.SciSerialEdit import TextEdit
from conf.configuration import logger, DiagsPrompt, IBootPrompt, OS_Prefix, \
    SYSD_PATH, Astro_PATH, END_Signal, ModesPromt, Read_Delay, re_cur_color_bac
from script.tcprelay import TcpRelay


class SerialFrame(Frame):
    send_cmd_signal = pyqtSignal(str)  # 发送命令给serial
    select_serial_signal = pyqtSignal(int)
    set_status_signal = pyqtSignal(str)  # 传递模式名到statusBar
    output_signal = pyqtSignal(str)
    dir_signal = pyqtSignal(str)
    jayson_output_signal = pyqtSignal(str)
    note_box_list = []

    def __init__(self, name='', *args, **kwargs):
        self.name = name
        self._serial = None
        # -------- customize for Jayson.xu --------
        self._cur_cmd = ''
        self._cur_output = ''
        self.jayson_cmds = ''
        self.cmd_outputs_fp = ''

        self.sdbug = SerialDebug(self)
        self.confirm_mode_thread = ConfirmStatus(self)
        super(SerialFrame, self).__init__(name=name, *args, **kwargs)
        self.jayson_cmd_status = 0
        self.setObjectName('serialpage')
        self.note_box = NoteBox()
        self.note_box_list.append(self.note_box)

        self.t = self.findChild(TextEdit)
        self.m = self.findChild(MiniMap)
        self._setup_signal()
        self.sdbug.get_avaliable_ports()  # 获取串口列表

        self._ssh = None
        self._ssh2 = None  # for file transfer
        self._ssh_opened = 0
        self._treeview = None
        self._tree_opened = 0
        self._treeview_dir_status = 0  # treeview dir nandfs cmd status

        self.tcprelay = TcpRelay(0)  # 传入默认的值
        self.tcprelay.open()
        self.port_offset = self.tcprelay.portoffset()

        self.read_timer = QTimer(self)
        self.line_cont = ''  # 存储读取到的内容

        self.read_b_timer = QTimer(self)
        self.read_b_timer.timeout.connect(self.read_timer_timeout)
        # self.read_timer.start(self.read_delay*2)
        self.read_timer.timeout.connect(self.read_timer_timeout)
        self.cmd_outputs_fp_timer = QTimer(self)
        self.cmd_outputs_fp_timer.timeout.connect(self.del_cmd_outputs_fp)

    def setup_ui(self):
        self.m = MiniMap(self.name)
        self.t = TextEdit(self.name, event=True)
        self.m.sBar.s_signal.connect(self.t.sBar.set_slider_pos)
        self.t.sBar.s_signal.connect(self.m.sBar.set_slider_pos)

        lay = QHBoxLayout(self)
        lay.setContentsMargins(0, 0, 0, 0)
        lay.setSpacing(0)
        l_vlay = QVBoxLayout()
        l_vlay.setObjectName('vlay')
        l_vlay.setContentsMargins(0, 0, 0, 0)
        l_vlay.setSpacing(0)
        lay.addLayout(l_vlay)
        l_vlay.addWidget(self.search_bar)
        l_vlay.addWidget(self.t)
        lay.addWidget(self.m)
        self.search_bar.hide()  # 先隐藏

    def _setup_signal(self):
        self.send_cmd_signal.connect(self.sdbug.send_cmd)
        self.sdbug.got_port_signal.connect(self.show_serial_list)
        self.sdbug.set_serial_signal.connect(self.set_serial)  # 选择串口
        self.select_serial_signal.connect(self.sdbug.connect_to_port)  # 发送串口序号，连接串口
        self.t.key_pressed_signal.connect(self.send_cmd)
        self.confirm_mode_thread.set_mode_signal.connect(self.mode)
        self.t.tab_pressed_signal.connect(self.send_cmd)
        self.t.fun_pressed_signal.connect(self.send_cmd, Qt.UniqueConnection)
        # self.output_signal.connect(self.t.recv_)
        self.jayson_output_signal.connect(self.save_output)

    def mode(self, mode: str):
        if self.t.mode != mode:
            self.t.mode = mode
            self.t.mode_switched(self.t.mode, mode)
        # if mode == 'OS' and not self._ssh_opened:
        #     self.openSSH()

    def ensure_tcprelay_alive(self):
        is_port_active = self.tcprelay.is_active
        logger.debug(f"Is port active? {is_port_active}")
        if not is_port_active:
            self.tcprelay = TcpRelay(0)  # 传入默认的值
            self.tcprelay.open()
            self.port_offset = self.tcprelay.portoffset()

    def ssh_as_root(self):
        self._ssh = SSHEdit(port_offset=self.port_offset)
        self._ssh.close_signal.connect(self.sub_window_closed)
        self.SSHWIN.append(self._ssh)
        self._ssh_opened = ~self._ssh_opened
        self._ssh.show()

    SSHWIN = []

    def openSSH(self):
        logger.debug(f"Opening ssh")
        try:
            if self.t.mode == 'OS' and not self._ssh_opened:
                self.ensure_tcprelay_alive()
                self.ssh_as_root()
            elif self.t.mode == 'Diags':
                self.note_box.warning(f"Diags mode don't support ssh.")
            else:
                try:
                    self.ensure_tcprelay_alive()
                    self.ssh_as_root()
                except Exception as e:
                    pass
                logger.debug(f"Not assure the status of DUT, tried to ssh as root and result: {self.SSHWIN}")
        except Exception as e:
            logger.error(e, exc_info=1)
            self.note_box.warning(str(e))
            # self.

    def open_treeview(self):
        try:
            if not self._tree_opened and self.t.mode in ['OS', 'Diags']:
                logger.debug(f"Opening treeview")
                self._treeview = TreeView(parent=self, port_offset=self.port_offset, mode=self.t.mode)
                self._treeview.close_signal.connect(self.sub_window_closed)
                self.SSHWIN.append(self._treeview)
                self.dir_signal.connect(self._treeview.set_dir_list)
                self._tree_opened = ~self._tree_opened
                self._treeview.show()
            else:
                logger.debug(self.SSHWIN)
        except Exception as e:
            logger.error(e, exc_info=1)
            self.note_box.warning(e.args[1])

    @pyqtSlot(str)
    def sub_window_closed(self, widget_name: str):
        logger.debug(f"Reseting widget {widget_name} status.")
        if widget_name == 'treeview':
            self._treeview = None
            self._tree_opened = ~self._tree_opened
        elif widget_name == 'sshedit':
            self._ssh = None
            self._ssh_opened = ~self._ssh_opened

        # [self.SSHWIN.remove(_w) for _w in self.SSHWIN if _w.objectName() == widget_name]
        logger.debug(f"Reseting widget {widget_name} status OK")

    def switch_dir_status(self, lno: int = 0):
        self._treeview_dir_status = 0 if self._treeview_dir_status else 1
        logger.debug(f"dir status switch to {repr(self._treeview_dir_status)}, lno: {lno}")
        return self._treeview_dir_status

    def confirm_mode(self, cont: str):
        self.confirm_mode_thread.cont = cont
        if not self.confirm_mode_thread.isRunning():
            self.confirm_mode_thread.run()

    def show_serial_list(self, serial_list: dict):
        for num, serial in enumerate(serial_list):
            self.show_output(f"Serial ({num}): {serial}\n")
        self.show_output(f"Select a device by its number:")

    def send_cmd(self, cmd: str):
        logger.debug(repr(cmd))
        if self._serial:
            self.send_cmd_signal.emit(cmd)
        else:
            try:
                self.select_serial_signal.emit(int(cmd))
            except ValueError as e:
                logger.error(e)

    def set_serial(self, serial: dict):
        logger.debug(f'setting serial signal')
        self._serial = serial.get('serial')
        self._port = serial.get('port_name')
        self.t.title_pySignal.emit(self._port)
        logger.debug(f"emitted {self._port}")
        self._serial.readyRead.connect(self.on_ready_read)
        # self.th = ReadSerial(self, serial=self._serial)
        # self.th.recv_data_sig.connect(self.on_ready_read)
        # self.th.start()

    def show_output(self, cont: str):
        cont = cont.replace('WARNING: Failed to read bytes:Time out\n', '')
        # logger.debug(f"{repr(cont)}")
        # self.t.moveCursor(QTextCursor.End, QTextCursor.MoveAnchor)
        self.t.insert_at_end(cont)
        self.m.insert_at_end(cont)
        self.deal_output_w_jayson(cont)

    def insert(self, cont: str):
        cont = cont.replace('WARNING: Failed to read bytes:Time out\n', '')
        # logger.debug(f"{repr(cont)}")
        self.t.insert(cont)
        self.m.insert(cont)
        self.deal_output_w_jayson(cont)

    # ------- BEGIN Customize for Jayson -------
    def deal_output_w_jayson(self, cont):
        if self.jayson_cmd_status and ModesPromt.get(self.t.mode).search(cont):  # 输出Diag标识符并且Jayson命令状态为真的情况
            logger.debug(f"监测到命令运行结束")
            self.jayson_output_signal.emit(END_Signal)
        elif self.jayson_cmd_status:
            self.jayson_output_signal.emit(cont)

    def del_cmd_outputs_fp(self):
        self.cmd_outputs_fp_timer.stop()
        self.cmd_outputs_fp = ''

    def run_jayson_cmds(self, cmds: str):
        self.jayson_cmd_status = 1 if not self.jayson_cmd_status else ''
        logger.debug(f"Switch jayson cmd status to {self.jayson_cmd_status}")
        self.jayson_cmds = (i for i in cmds.split('\n'))
        self.cmd_outputs_fp = QFileDialog.getSaveFileName(
            self, 'Save Commands outputs',
            os.path.expanduser(f'~/Desktop/newFile.csv'), "CSV files (*.csv)")[0]
        if self.cmd_outputs_fp:
            logger.debug(f"fp: {repr(self.cmd_outputs_fp)}")
            os.remove(self.cmd_outputs_fp) if os.path.exists(self.cmd_outputs_fp) else ''  # 如果文件已经存在，则移除以覆盖
            self.next_cmds()
        else:
            logger.warning(f"null fp")
            return

    def next_cmds(self):
        try:
            cmd = next(self.jayson_cmds)
            logger.debug(f"Run cmd: {cmd}")
            self.cur_cmd = cmd
            self.send_cmd(cmd.strip()+'\n')
        except StopIteration:  # 命令运行结束，清空一些变量
            self.jayson_cmd_status = 0 if self.jayson_cmd_status else ''
            self.cur_cmd = ''
            self._cur_output = ''
            self.note_box.info(f"Cmds run over, Please see file :{self.cmd_outputs_fp}")
            self.cmd_outputs_fp_timer.start(1000)
            logger.debug(f"Switch jayson cmd status to {self.jayson_cmd_status}")
            logger.info(f"Jayson cmds run over!")

    def save_output(self, output: str):
        logger.debug(f"Received: {repr(output)}")
        if output != END_Signal:
            self._cur_output += output
        else:
            # self.cmd_outputs_fp = self.cmd_outputs_fp or \
            #                       QFileDialog.getSaveFileName(
            #                           self, 'Save Commands outputs',
            #                           os.path.expanduser(f'~/Desktop/newFile.csv'), "CSV files (*.csv)")[0]
            if not self.cmd_outputs_fp:
                self.note_box.warning(f"Not selected file path.")
                return
            with open(self.cmd_outputs_fp, 'a', newline='') as f:
                f_csv = csv.writer(f)
                f_csv.writerow([self.cur_cmd, self._cur_output])
                logger.debug(f"Writing : {self.cur_cmd, self._cur_output}")
            self._cur_output = ''
            self.next_cmds()

    @property
    def cur_cmd(self):
        return self._cur_cmd

    @cur_cmd.setter
    def cur_cmd(self, v: str):
        logger.info(f"Current running cmd: {v}")
        self._cur_cmd = v
    # ------- END Customize for Jayson -------

    def get_sysdiagnose(self):
        """在cmd窗口运行这个菜单，可以直接copy sysdiagnose到本地"""
        if self.t.mode == 'OS':
            if not self._ssh2:
                self.ensure_tcprelay_alive()
                self._ssh2 = SSH(self.port_offset, self)
            fp = QFileDialog.getExistingDirectory(self, 'Save sysdiagnose to', os.path.expanduser(f'~/Desktop/sysdiagnose'),
                                                  options=QFileDialog.ShowDirsOnly)
            if fp:
                self._ssh2.task = {'method': 'getd', 'src': SYSD_PATH, 'dst': fp}
                # self._ssh2.getd(SYSD_PATH, fp)
                self._ssh2.start()
            else:
                logger.error(f"Error: fp {repr(fp)}")
        elif self.t.mode =='Diags':
            self.note_box.warning(f"请使用{repr('Command+B')}调用远程文件管理器")
        else:
            pass

    def get_astro(self):
        """在cmd窗口运行这个菜单，可以直接copy sysdiagnose到本地"""
        if self.t.mode == 'OS':
            self.ensure_tcprelay_alive()
            if not self._ssh2:
                self._ssh2 = SSH(self.port_offset, self)
            fp = QFileDialog.getExistingDirectory(self, 'Save Astro to', os.path.expanduser(f'~/Desktop/Astro'),
                                                  options=QFileDialog.ShowDirsOnly)
            if fp:
                self._ssh2.task = {'method': 'getd', 'src': Astro_PATH, 'dst': fp}
                # self._ssh.getd(Astro_PATH, fp)
                self._ssh2.start()
            else:
                logger.error(f"Error: fp {repr(fp)}")
        elif self.t.mode =='Diags':
            self.note_box.warning(f"请使用{repr('Command+B')}调用远程文件管理器")
        else:
            pass

    def on_ready_read(self, r=''):
        r = self._serial.readAll()
        logger.debug(repr(r))
        r = bytes(r).decode('utf-8', errors='ignore')
            # .replace('\r', '')

        # self.show_output(r)
        # return

        self.line_cont += r if not self.t.no_need_echo_status() else ''
        self.restart_read_timer()
        if not self.line_cont.__contains__('\n'):
            # 等待换行符
            return
        *_r, self.line_cont = self.line_cont.splitlines(True)
        for __r in _r:
            __r = re_cur_color_bac.sub('', __r)
            # logger.debug(f"\033[33m{repr(__r)}\033[0m")

            self.confirm_mode(__r)
            self.show_output(__r)
            if self._treeview_dir_status:
                self.dir_signal.emit(__r)
            # self.recv_signal.emit(r)
            if self.t.tab_status:
                if __r.startswith(self.t.cmd_long) and self.t.cmd_long:
                    self.t.cmd_long = __r.replace('\x07', '')

    def read_timer_timeout(self):
        """读取超时,处理未成行的数据"""
        self.read_timer.stop()
        if self.line_cont and not self.t.no_need_echo_status():
            # cont, self.line_cont = replace_unconditionally_str.sub('', self.line_cont), ''
            cont, self.line_cont = self.line_cont, ''
            cont = re_cur_color_bac.sub('', cont)
            logger.debug(repr(cont))

            if not cont:
                logger.debug(f"no content, return")
                return
            self.confirm_mode(cont)  # 确保监测模式变化的QThread在运行
            self.t.estimate_len_of_os_prefix(cont)  # 更新OS_Prefix
            cont = self.t.deal_by_status(cont)

            self.insert(cont)
            if self._treeview_dir_status and DiagsPrompt.search(cont):
                self.dir_signal.emit('->end<-')
                self.switch_dir_status(403)

    def restart_read_timer(self):
        if self.read_timer.isActive():
            self.read_timer.stop()
        self.read_timer.start(Read_Delay)

    @pyqtSlot(dict)
    def note_box_(self, v: dict):
        """v={m: info/warning, c: content}"""
        getattr(self.note_box, v.get('m'))(v.get('c'))

    # def hideEvent(self, a0) -> None:
    #     self.close()

    def closeEvent(self, a0: QCloseEvent) -> None:
        logger.debug(f"Closing {self.name}")
        if self._serial:  # 触发关闭事件时可能还没有选择串口对象
            if self._serial.isOpen():
                logger.debug(f'Closing serial {self._serial}')
                self._serial.close()
            else:
                logger.warning(f"serial status: {self.t.serial_opened}")
        else:
            logger.warning(f"no serial: self._serial -> {repr(self._serial)}")
        return super(SerialFrame, self).closeEvent(a0)


class SerialDebug(QObject):
    got_port_signal = pyqtSignal(dict)  # 发送获取到的port list 信息
    set_serial_signal = pyqtSignal(dict)  # 发送serial信息

    def __init__(self, parent):
        super(SerialDebug, self).__init__()
        self._parent = parent
        self._ports = {}  # 用于保存串口的信息
        self._serials = {}  # 串口组对象

    def get_avaliable_ports(self):
        # 获取可用的串口
        infos = QSerialPortInfo().availablePorts()
        infos.reverse()  # 逆序
        for info in infos:
            if info.systemLocation().__contains__('-ch-0') and info.systemLocation().__contains__('cu.'):
                self._ports[info.systemLocation()] = info
        logger.debug(f"Got serial list: {self._ports}")
        self.got_port_signal.emit(self._ports)

    def connect_to_port(self, port_sequence: int):
        logger.debug(f'Received {port_sequence}')
        self._parent.t.insert_at_end(str(port_sequence))
        try:
            port = list(self._ports)[port_sequence]
        except IndexError:
            return
        _serial = QSerialPort()
        _serial.setObjectName(port)
        _serial.setPortName(port)
        _serial.setBaudRate(QSerialPort.Baud115200)
        _serial.setParity(QSerialPort.NoParity)
        _serial.setDataBits(QSerialPort.Data8)
        _serial.setStopBits(QSerialPort.OneStop)
        _serial.setFlowControl(QSerialPort.NoFlowControl)  # ???
        ok = _serial.open(QIODevice.ReadWrite)
        if ok:
            connect_prompt_str = f"\n****** Opened serial {port} ******\n"
            logger.debug(connect_prompt_str)
            self._parent.t.serial_opened = True
            self._parent.show_output(connect_prompt_str)
            self._serials.__setitem__('port_name', port)
            self._serials.__setitem__('serial', _serial)
            self._serials.__setitem__('is_open', 1)
            self.set_serial_signal.emit(self._serials)
        else:
            QMessageBox().warning(self._parent, 'Failed', f'Open serial {port} failed, maybe serial is busy now.', QMessageBox.Cancel)
            logger.debug(f"Open serial failed.")

    def send_cmd(self, port_cmd: str):
        # logger.debug(repr(port_cmd))
        serial: QSerialPort
        serial = self._serials.get('serial')
        state = self._serials.get('is_open')
        cmd = port_cmd
        if serial.isOpen() and state == 1:
            text = QByteArray(cmd.encode('utf-8'))  # iso-8859-1
            serial.write(text)
            self._parent.restart_read_timer()
        else:
            logger.warning(f"serial {port_cmd} isOpen: {serial.isOpen()} state: {state}")


class ConfirmStatus(QThread):
    set_mode_signal = pyqtSignal(str)

    def __init__(self, parent, *args, **kwargs):
        super(ConfirmStatus, self).__init__(*args, **kwargs)
        self._parent = parent
        self._cont = ''

    def run(self) -> None:
        while self._cont:
            # logger.debug(f"confirm status: {self._cont}")
            if OS_Prefix.search(self._cont):
                status = "OS"
                self.set_mode_signal.emit(status)
            elif DiagsPrompt.search(self._cont):
                diags_prefix = DiagsPrompt.findall(self._cont)[0]
                if self._parent.t.diags_prefix != diags_prefix:
                    self._parent.t.diags_prefix = diags_prefix
                    logger.info(f"locate diags prefix: {self._parent.t.diags_prefix}")
                status = 'Diags'
                self.set_mode_signal.emit(status)
            elif IBootPrompt.search(self._cont):
                status = 'Recovery'
                self.set_mode_signal.emit(status)
            else:
                status = ''
            # logger.debug(f"status: {status}")
            self._cont = ''
            if status:
                self._parent.set_status_signal.emit(status)

    @property
    def cont(self):
        return self._cont

    @cont.setter
    def cont(self, cont):
        self._cont += cont


class ReadSerial(QThread):
    recv_data_sig = pyqtSignal(QByteArray)

    def __init__(self, *args, **kwargs):
        super(ReadSerial, self).__init__(*args)
        self.serial: QSerialPort = kwargs.get('serial')

    def run(self) -> None:
        self.serial.readyRead.connect(self.on_ready_read)

    def on_ready_read(self):
        r = self.serial.readAll()
        self.recv_data_sig.emit(r) if r else ''


if __name__ == '__main__':
    app = QApplication(sys.argv)
    se = SerialFrame()
    se.setFixedSize(1000, 750)
    se.show()
    sys.exit(app.exec_())

    # SD = SerialDebug()
    # SD.get_avaliable_ports()
